Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit dd344a9e3359973668e1f733c980665f68b4ff69


Parents : c52d429
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-04-23T04:30:16-05:00

feat(security): implement path validation for shell commands to restrict access to allowed directories

Changes

3 files changed, 139 insertions(+), 9 deletions(-)


Diff

diff --git a/electron/main-legacy.js b/electron/main-legacy.js
index 2da1d83f..fe2ec687 100644
--- a/electron/main-legacy.js
+++ b/electron/main-legacy.js
@@ -13,6 +13,8 @@ const { spawn } = require("child_process");
const fs = require("fs");
const path = require("node:path");
const crypto = require("crypto");
+const { getUserProvidedArguments } = require("./mainHelpers");
+const { isAllowedShellPath } = require("./shellPathGuard");
// remember main window
var mainWindow = null;
@@ -197,12 +199,32 @@ ipcMain.handle("get-memory-usage", async () => {
});
// allow showing a file path in os file manager
-ipcMain.handle("showPathInFolder", (event, path) => {
- shell.showItemInFolder(path);
+ipcMain.handle("showPathInFolder", (event, targetPath) => {
+ const ctx = {
+ app,
+ getDefaultStorageDir,
+ getDefaultReticulumConfigDir,
+ getUserProvidedArguments,
+ };
+ if (!isAllowedShellPath(targetPath, ctx)) {
+ console.warn("showPathInFolder denied (path outside allowed directories)");
+ return;
+ }
+ shell.showItemInFolder(targetPath);
});
-ipcMain.handle("open-path", (event, p) => {
- return shell.openPath(p);
+ipcMain.handle("open-path", (event, targetPath) => {
+ const ctx = {
+ app,
+ getDefaultStorageDir,
+ getDefaultReticulumConfigDir,
+ getUserProvidedArguments,
+ };
+ if (!isAllowedShellPath(targetPath, ctx)) {
+ console.warn("open-path denied (path outside allowed directories)");
+ return "Path is not allowed";
+ }
+ return shell.openPath(targetPath);
});
ipcMain.handle("pick-file", async () => {

diff --git a/electron/main.js b/electron/main.js
index 4d82f299..0188af80 100644
--- a/electron/main.js
+++ b/electron/main.js
@@ -19,6 +19,7 @@ const path = require("node:path");
const { verifyBackendIntegrity } = require("./backendIntegrity");
const { getUserProvidedArguments, formatRenderProcessGoneDetails, isLocalBackendUrl } = require("./mainHelpers");
+const { isAllowedShellPath } = require("./shellPathGuard");
// remember main window
var mainWindow = null;
@@ -261,12 +262,32 @@ ipcMain.handle("get-memory-usage", async () => {
});
// allow showing a file path in os file manager
-ipcMain.handle("showPathInFolder", (event, path) => {
- shell.showItemInFolder(path);
+ipcMain.handle("showPathInFolder", (event, targetPath) => {
+ const ctx = {
+ app,
+ getDefaultStorageDir,
+ getDefaultReticulumConfigDir,
+ getUserProvidedArguments,
+ };
+ if (!isAllowedShellPath(targetPath, ctx)) {
+ console.warn("showPathInFolder denied (path outside allowed directories)");
+ return;
+ }
+ shell.showItemInFolder(targetPath);
});
-ipcMain.handle("open-path", (event, p) => {
- return shell.openPath(p);
+ipcMain.handle("open-path", (event, targetPath) => {
+ const ctx = {
+ app,
+ getDefaultStorageDir,
+ getDefaultReticulumConfigDir,
+ getUserProvidedArguments,
+ };
+ if (!isAllowedShellPath(targetPath, ctx)) {
+ console.warn("open-path denied (path outside allowed directories)");
+ return "Path is not allowed";
+ }
+ return shell.openPath(targetPath);
});
ipcMain.handle("pick-file", async () => {
@@ -467,7 +488,7 @@ app.whenReady().then(async () => {
// Define a robust fallback CSP that matches our backend's policy
const fallbackCsp = [
"default-src 'self'",
- "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
+ "script-src 'self' 'unsafe-inline'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob: https://*.tile.openstreetmap.org https://tile.openstreetmap.org https://*.cartocdn.com https://tiles.openfreemap.org https://*.openfreemap.org",
"font-src 'self' data: https://tiles.openfreemap.org https://*.openfreemap.org",

diff --git a/electron/shellPathGuard.js b/electron/shellPathGuard.js
new file mode 100644
index 00000000..93917b93
--- /dev/null
+++ b/electron/shellPathGuard.js
@@ -0,0 +1,87 @@
+"use strict";
+
+const fs = require("fs");
+const path = require("node:path");
+
+function parseArgvFlag(argv, flagName) {
+ const list = Array.isArray(argv) ? argv : [];
+ const idx = list.indexOf(flagName);
+ if (idx === -1 || idx + 1 >= list.length) {
+ return null;
+ }
+ const v = list[idx + 1];
+ if (!v || v.startsWith("--")) {
+ return null;
+ }
+ return v;
+}
+
+function resolveDirForPrefixCheck(dirPath) {
+ try {
+ if (typeof fs.realpathSync.native === "function") {
+ return fs.realpathSync.native(dirPath);
+ }
+ return fs.realpathSync(dirPath);
+ } catch {
+ return path.resolve(dirPath);
+ }
+}
+
+function isResolvedPathUnderRoot(resolvedCandidate, rootPath) {
+ const root = resolveDirForPrefixCheck(rootPath);
+ const file = path.resolve(resolvedCandidate);
+ const rel = path.relative(root, file);
+ return rel === "" || (!rel.startsWith(`..${path.sep}`) && rel !== "..");
+}
+
+/**
+ * @param {string} targetPath
+ * @param {object} ctx
+ * @param {import("electron").App} ctx.app
+ * @param {() => string} ctx.getDefaultStorageDir
+ * @param {() => string} ctx.getDefaultReticulumConfigDir
+ * @param {(argv: string[]) => string[]} ctx.getUserProvidedArguments
+ * @returns {boolean}
+ */
+function isAllowedShellPath(targetPath, ctx) {
+ if (typeof targetPath !== "string" || !targetPath.trim()) {
+ return false;
+ }
+ if (targetPath.includes("\0")) {
+ return false;
+ }
+ const resolved = path.resolve(targetPath.trim());
+ const roots = [];
+ const add = (p) => {
+ if (p) {
+ roots.push(p);
+ }
+ };
+
+ add(ctx.getDefaultStorageDir());
+ add(ctx.getDefaultReticulumConfigDir());
+ add(ctx.app.getPath("userData"));
+ add(ctx.app.getPath("temp"));
+ add(ctx.app.getPath("downloads"));
+ add(ctx.app.getPath("documents"));
+
+ const portable = process.env.PORTABLE_EXECUTABLE_DIR;
+ if (portable) {
+ add(portable);
+ }
+
+ const userArgv = ctx.getUserProvidedArguments(process.argv);
+ add(parseArgvFlag(userArgv, "--storage-dir"));
+ add(parseArgvFlag(userArgv, "--reticulum-config-dir"));
+
+ for (const root of roots) {
+ if (root && isResolvedPathUnderRoot(resolved, root)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+module.exports = {
+ isAllowedShellPath,
+};


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────